﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Reflection;
using Lidgren.Network;
using System.Runtime.InteropServices;
using Microsoft.Xna.Framework;
using Lidgren.Network.Xna;


namespace Kaleta.Network
{
    /// <summary>
    /// Serializes and deserializes Distributable objects for network transfer.
    /// </summary>
    public class DistributionSerializer
    {
        static internal void Serialize(NetBuffer sendBuffer, DistributionType typeOfDistribution, 
            DistributableObject objToSerialize, PropertyInfo propertyInfo, 
            object newValue, NetChannel? netChannel)
        {
            //Serialized object has this info:
            //0)type of transfer: reliable, unreliable, ..
            //a)Type of operation on object: CreateNew, Update, Disposal
            //b)Object's Key
            //c)Properties to update OR for creating new instance (type of class can be retrieved from propertyInfo)
            //d)New values on these properties 

            if(netChannel==null)
                sendBuffer.Write((byte)NetChannel.Unreliable);
            else
                sendBuffer.Write((byte)netChannel);

            sendBuffer.Write((byte)typeOfDistribution);
            sendBuffer.Write(objToSerialize.Key);
            sendBuffer.Write(PropertyTypesToIndexes[propertyInfo]);

            if (typeOfDistribution == DistributionType.Update)
            {
                if (propertyInfo.PropertyType.Equals(typeof(int)))
                    sendBuffer.Write((int)newValue);
                else if (propertyInfo.PropertyType.Equals(typeof(float)))
                    sendBuffer.Write((float)newValue);
                else if (propertyInfo.PropertyType.Equals(typeof(double)))
                    sendBuffer.Write((double)newValue);
                else if (propertyInfo.PropertyType.Equals(typeof(byte)))
                    sendBuffer.Write((byte)newValue);
                else if (propertyInfo.PropertyType.Equals(typeof(bool)))
                {
                    byte[] bytes = BitConverter.GetBytes((bool)newValue);
                    sendBuffer.Write(bytes[0]);
                }
                else if (propertyInfo.PropertyType.Equals(typeof(string)))
                    sendBuffer.Write((string)newValue);
                else if (propertyInfo.PropertyType.Equals(typeof(Vector3)))
                    XnaSerialization.Write(sendBuffer, (Vector3)newValue);

                else if (propertyInfo.PropertyType.Equals(typeof(Quaternion)))
                    XnaSerialization.WriteRotation(sendBuffer, (Quaternion)newValue,24);

                else
                    throw new DistributableObjectException("Cannot serialize type of " + propertyInfo.PropertyType.Name);
            }


        }

        /// <summary>
        /// Deserializes data from readBuffer, updates dictionary. If resend Buffer in not null, rewrites data to it.
        /// </summary>
        static internal void Deserialize(NetBuffer readBuffer, Dictionary<int, DistributableObject> dict, NetBuffer[] resendBuffer)
        {
            //read the channel:
            NetChannel netChannel = (NetChannel) readBuffer.ReadByte();

            //if (resendBuffer != null)
            //    resendBuffer[(int)netChannel].Write(readBuffer.ToArray());
            
            //read type of distribution:
            DistributionType typeOfDistribution = (DistributionType)readBuffer.ReadByte();

            //read the Key for dictionary:
            int key = readBuffer.ReadInt32();

            //read the property info:
            ushort propType = readBuffer.ReadUInt16(); //.ReadByte();
            PropertyInfo propertyInfo = IndexesToPropertyTypes[propType];

            //deserialize furher data only if Update:
            DistributableObject obj;

            //Update:
            if (typeOfDistribution == DistributionType.Update)
            {
                //DistributableObject dictObj;

                //if we dont have this object, create a temp object to poll the data from buffer and then discard this obj:
                if (dict.TryGetValue(key, out obj) == false)
                    obj = Activator.CreateInstance(propertyInfo.DeclaringType) as DistributableObject;

                //disable propagation of setters:
                obj.Distribute = false;
                object newValue;

                if (propertyInfo.PropertyType.Equals(typeof(int)))
                    newValue = readBuffer.ReadInt32();

                else if (propertyInfo.PropertyType.Equals(typeof(float)))
                    newValue = readBuffer.ReadFloat();

                else if (propertyInfo.PropertyType.Equals(typeof(byte)))
                    newValue = readBuffer.ReadByte();

                else if (propertyInfo.PropertyType.Equals(typeof(bool)))
                {
                    byte b = readBuffer.ReadByte();
                    if (b == 0)
                        newValue = false;
                    else
                        newValue = true;
                }
                else if (propertyInfo.PropertyType.Equals(typeof(double)))
                    newValue = readBuffer.ReadDouble();

                else if (propertyInfo.PropertyType.Equals(typeof(string)))
                    newValue = readBuffer.ReadString();

                else if (propertyInfo.PropertyType.Equals(typeof(Vector3)))
                    newValue = readBuffer.ReadVector3();

                else if (propertyInfo.PropertyType.Equals(typeof(Quaternion)))
                    newValue = readBuffer.ReadRotation(24);

                else
                    throw new DistributableObjectException("Cannot deserialize type of " + propertyInfo.PropertyType.Name);

                propertyInfo.SetValue(obj, newValue, null);

                obj.Distribute = true;

                if (resendBuffer != null)
                    Serialize(resendBuffer[(int)netChannel],DistributionType.Update, obj, propertyInfo, newValue, netChannel);
            }
            //Create:
            else if (typeOfDistribution == DistributionType.CreateNew)
            {
                //DistributableObject dictObj;

                //if we dont have this object, so create it:
                if (dict.TryGetValue(key, out obj) == false)
                {
                    obj = Activator.CreateInstance(propertyInfo.DeclaringType) as DistributableObject;
                    obj.Key = key;
                    obj.IsCreated = true;

                    dict.Add(obj.Key, obj);
                }
                else
                    obj.IsCreated = true;

                if(resendBuffer != null)
                    Serialize(resendBuffer[(int)netChannel], DistributionType.CreateNew, obj, obj.GetType().GetProperties()[0], null, NetChannel.ReliableUnordered);
            }
            else if (typeOfDistribution == DistributionType.Dispose)
            {
                //if we have this object, remove it from dictionary:
                if (dict.TryGetValue(key, out obj) == true)
                {
                    dict.Remove(obj.Key);

                    if (resendBuffer != null)
                        Serialize(resendBuffer[(int)netChannel], DistributionType.Dispose, obj, obj.GetType().GetProperties()[0], null, NetChannel.ReliableUnordered);
                }
            }
        }




        static internal void RegisterClass(Type type)
        {
            if (!type.IsSubclassOf(typeof(DistributableObject)))
                throw new DistributableObjectException("Type " + type.Name + " must be derived from DistributableObject");

            if (!DistributionSerializer.registeredClasses.Contains(type))
            {
                PropertyInfo[] infos = type.GetProperties();
                foreach (var info in infos)
                    DistributionSerializer.RegisterProperty(info);

                DistributionSerializer.registeredClasses.Add(type);
            }

        }

        static private void RegisterProperty(PropertyInfo type)
        {
            PropertyTypesToIndexes.Add(type, propertyTypeIndex);
            IndexesToPropertyTypes.Add(propertyTypeIndex, type);

            propertyTypeIndex++;
        }


        static private ushort propertyTypeIndex = 0;

        static private Dictionary<PropertyInfo, ushort> PropertyTypesToIndexes = new Dictionary<PropertyInfo, ushort>();
        static private Dictionary<ushort, PropertyInfo> IndexesToPropertyTypes = new Dictionary<ushort, PropertyInfo>();

        static private List<Type> registeredClasses = new List<Type>();


    }
}
